package com.hevs.samplewebapp.client.bean;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import com.google.gwt.i18n.shared.DateTimeFormat;
import com.google.gwt.user.datepicker.client.CalendarUtil;

/**
 * 
 * @author Thomas Rouvinez
 * @Date: 18.04.2012
 *
 * Goal: provide with an helper to create and bake statistics more easily.
 * We first bake the global statistics and then add to it the categorical
 * statistics.
 * 
 * This object is designed for single use only, do not reuse it but create
 * a new instance.
 */
public class StatisticsProcessingHelper {

	// -----------------------------------------------------------------------------------------------
	// Variables.
	// -----------------------------------------------------------------------------------------------

	/**
	 * Storage unit for the global data to receive from the web service.
	 */
	private MedicalDataItem[] globalData;
	
	/**
	 * Temporary storage unit for the medical data items once sorted and trimmed according to the
	 * specified dates.
	 */
	private MedicalDataItem[] tempData;

	/**
	 * Date object for the beginning date range.
	 */
	private Date fromDate = null;
	
	/**
	 * Date object for the end date range.
	 */
	private Date toDate = null;

	/**
	 *  Storage system for global mean statistics:<br>
	 *  [0] = bloodPressureSystolic<br>
	 *  [1] = bloodPressureDiastolic<br>
	 *  [2] = bloodSugar<br>
	 *  [3] = humalog<br>
	 *  [4] = novorapid<br>
	 *  [5] = weight<br>
	 *  [6] = pulseRate
	 */
	private double mean[] = new double[7];
	
	/**
	 *  Storage system for global minimum statistics:<br>
	 *  [0] = bloodPressureSystolic<br>
	 *  [1] = bloodPressureDiastolic<br>
	 *  [2] = bloodSugar<br>
	 *  [3] = humalog<br>
	 *  [4] = novorapid<br>
	 *  [5] = weight<br>
	 *  [6] = pulseRate
	 */
	private double min[] = {999.0, 999.0, 999.0, 999.0, 999.0, 999.0, 999.0};
	
	/**
	 *  Storage system for global maximum statistics:<br>
	 *  [0] = bloodPressureSystolic<br>
	 *  [1] = bloodPressureDiastolic<br>
	 *  [2] = bloodSugar<br>
	 *  [3] = humalog<br>
	 *  [4] = novorapid<br>
	 *  [5] = weight<br>
	 *  [6] = pulseRate
	 */
	private double max[] = {-999.0, -999.0, -999.0, -999.0, -999.0, -999.0, -999.0};
	
	/**
	 *  Storage system for global median statistics:<br>
	 *  [0] = bloodPressureSystolic<br>
	 *  [1] = bloodPressureDiastolic<br>
	 *  [2] = bloodSugar<br>
	 *  [3] = humalog<br>
	 *  [4] = novorapid<br>
	 *  [5] = weight<br>
	 *  [6] = pulseRate
	 */
	private double median[] = new double[7];
	
	/**
	 *  Storage system for global standard deviation statistics:<br>
	 *  [0] = bloodPressureSystolic<br>
	 *  [1] = bloodPressureDiastolic<br>
	 *  [2] = bloodSugar<br>
	 *  [3] = humalog<br>
	 *  [4] = novorapid<br>
	 *  [5] = weight<br>
	 *  [6] = pulseRate
	 */
	private double standardDeviation[] = new double[7];

	/**
	 * Storage system for categorical statistics:<br>
	 * [0] = bloodSugar<br>
	 * [1] = humalog<br>
	 * [2] = novorapid
	 */
	private StatisticsHelper[][] categoryStatistics = new StatisticsHelper[3][3];
	
	/**
	 * Default value for the mean in categorical statistics.
	 */
	private double meanCat =  Double.NaN;
	
	/**
	 * Default value for the minimum in categorical statistics.
	 */
	private double minCat = 999.0;
	
	/**
	 * Default value for the maximum in categorical statistics.
	 */
	private double maxCat = -999.0;
	
	/**
	 * Default value for the median in categorical statistics.
	 */
	private double medianCat = Double.NaN;
	
	/**
	 * Default value for the standard deviation in categorical statistics.
	 */
	private double standardDeviationCat = Double.NaN;

	/**
	 * Temporary values for global statistics.
	 */
	@SuppressWarnings("unchecked")
	private ArrayList<Double>[] processedData = (ArrayList<Double>[]) new ArrayList[7];
	
	/**
	 * Temporary values for global statistics.
	 */
	@SuppressWarnings("unchecked")
	private ArrayList<Double>[][] processedDataCategory = (ArrayList<Double>[][]) new ArrayList[3][3];

	/**
	 * Initialization values.
	 */
	private double meanSum = 0.0, minValue = 999.0, maxValue = -999.0;

	// -----------------------------------------------------------------------------------------------
	// Constructors.
	// -----------------------------------------------------------------------------------------------

	/**
	 * Simple constructor with only initialization of vectors.
	 */
	public StatisticsProcessingHelper(){
		initStorage();
	}

	/**
	 * Constructor with specified data input and initialization of vectors.
	 * @param globalData
	 */
	public StatisticsProcessingHelper(MedicalDataItem[] globalData){
		this.globalData = globalData;
		initStorage();
	}
	
	/**
	 * Constructor with specified data input and initialization of beginning date
	 * and ending date for the time range.
	 * @param globalData
	 * @param fromDate
	 * @param toDate
	 */
	public StatisticsProcessingHelper(MedicalDataItem[] globalData, Date fromDate,
			Date toDate){
		this.globalData = globalData;
		this.fromDate =  fromDate;
		this.toDate = toDate;
		initStorage();
	}
	
	/**
	 * Method to initialize vector of arrayLists.
	 */
	private void initStorage(){
		for(int i = 0 ; i < processedData.length ; ++i){
			processedData[i] = new ArrayList<Double>();
		}

		for(int j = 0 ; j < processedDataCategory.length ; ++j){
			for(int k = 0 ; k < processedDataCategory[0].length ; ++k){
				processedDataCategory[j][k] = new ArrayList<Double>();
			}
		}
	}

	// -----------------------------------------------------------------------------------------------
	// Methods.
	// -----------------------------------------------------------------------------------------------

	/**
	 * Method to get the statistics from the medical data of a user according to a time range.
	 * @param daysCount
	 * @return StatisticsHelper with all statistics computed.
	 */
	public StatisticsHelper getStatistics(int daysCount){
		StatisticsHelper finalStatistics = new StatisticsHelper();

		try {		
			// Get new array with only the medicalData we need according to 'daysCount'.
			tempData = trim(daysCount);

			// Create separated list for each type of medical data from the trimmed list and store them.
			processDataGlobal(tempData);

			// Bake the global statistics.
			finalStatistics = bakeStatistics();

			// Reset values.
			meanSum = 0.0;
			minValue = 999.0;
			maxValue = -999.0;

			// Bake the categorical statistics and add them to the global statistics object.
			finalStatistics = bakeCategoryStatistics(finalStatistics);

		} catch (ParseException e) {
			e.printStackTrace();
		}

		return finalStatistics;
	}

	/**
	 * Method to get the custom statistics for specific date range.
	 * @return StatisticsHelper with all the custom statistics computed.
	 */
	public StatisticsHelper getCustomStatistics(){
		StatisticsHelper finalStatistics = new StatisticsHelper();

		try {		
			// Get new array with only the medicalData we need according to 'daysCount'.
			tempData = trimCustom(fromDate, toDate);

			// Create separated list for each type of medical data from the trimmed list and store them.
			processDataGlobal(tempData);

			// Bake the global statistics.
			finalStatistics = bakeStatistics();

			// Reset values.
			meanSum = 0.0;
			minValue = 999.0;
			maxValue = -999.0;

			// Bake the categorical statistics and add them to the global statistics object.
			finalStatistics = bakeCategoryStatistics(finalStatistics);

		} catch (ParseException e) {
			e.printStackTrace();
		}

		return finalStatistics;
	}
	
	/**
	 * Method to sort the input MedicalDataItem array into separated lists.
	 * @param selectedData
	 */
	private void processDataGlobal(MedicalDataItem[] selectedData){

		// Go through all data and make lists with all values.
		for(MedicalDataItem data : selectedData){

			switch (data.getType()) {
			case 0:
				processedData[2].add(data.getValue());
				processCategoricalValues(0, data);
				break;

			case 11:
				processedData[3].add(data.getValue());
				processCategoricalValues(1, data);
				break;

			case 12:
				processedData[4].add(data.getValue());
				processCategoricalValues(2, data);
				break;

			case 2:
				processedData[5].add(data.getValue());
				break;

			case 3:
				processedData[1].add(data.getValue());
				break;

			case 4:
				processedData[0].add(data.getValue());
				break;

			case 5:
				processedData[6].add(data.getValue());
				break;

			default:
				break;
			}
		}
	}

	/**
	 * Method to place a categorical medical data in the right storage system.
	 * @param type
	 * @param data
	 */
	private void processCategoricalValues(int type, MedicalDataItem data){
		// Check that the category is different from 'none' (0).
		if(data.getCategory() > 0){
			processedDataCategory[type][data.getCategory()-1].add(data.getValue());
		}
	}

	/**
	 * Method to bake global statistics.
	 * @return StatisticsHelper object with baked global statistics.
	 */
	private StatisticsHelper bakeStatistics(){
		StatisticsHelper result = new StatisticsHelper();

		// Bake overall statistics.
		for(int i = 0 ; i < processedData.length; ++i){
			if(processedData[i] != null){
				// Compute Values.
				for(int j = 0 ; j < processedData[i].size() ; j++){
					// Sum of the whole list.
					meanSum += processedData[i].get(j);

					// Check if we have the minimum value.
					if(minValue > processedData[i].get(j)){
						minValue = processedData[i].get(j);
					}

					// Check if we have the maximum value.
					if(maxValue < processedData[i].get(j)){
						maxValue = processedData[i].get(j);
					}
				}

				//Compute final values and store them.		
				mean[i] = meanSum / processedData[i].size();
				min[i] = minValue;
				max[i] = maxValue;
				median[i] = getMedian(processedData[i]);
				standardDeviation[i] = getStandardDeviation(processedData[i], median[i]);

				// Reset values.
				meanSum = 0.0;
				minValue = 999.0;
				maxValue = -999.0;
			}
		}

		// Save baked values.
		result.setMax(max);
		result.setMin(min);
		result.setMean(mean);
		result.setMedian(median);
		result.setStandardDeviation(standardDeviation);

		return result;
	}

	/**
	 * Method to bake category statistics.
	 * @param partiallyBakedStatistics
	 * @return a complete @see:StatisticsHelper object with both global and categorical
	 * statistics.
	 */
	private StatisticsHelper bakeCategoryStatistics(StatisticsHelper partiallyBakedStatistics){
		
		// Variables.
		StatisticsHelper result;
		StatisticsHelper stats = partiallyBakedStatistics;

		// For all the types of categorical data.
		for(int i = 0 ; i < processedDataCategory.length; ++i){

			// For all the categories in the selected type of categorical data.
			for(int j = 0 ; j < processedDataCategory[0].length ; ++j){
				result = new StatisticsHelper();

				// For all the data in the selected type and category.
				if(processedDataCategory[i][j] != null){

					// Compute Values.
					for(int k = 0 ; k < processedDataCategory[i][j].size() ; ++k){
						// Sum of the whole list.
						meanSum += processedDataCategory[i][j].get(k);

						// Check if we have the minimum value.
						if(minValue > processedDataCategory[i][j].get(k)){
							minValue = processedDataCategory[i][j].get(k);
						}

						// Check if we have the maximum value.
						if(maxValue < processedDataCategory[i][j].get(k)){
							maxValue = processedDataCategory[i][j].get(k);
						}
					}
	
					//Compute final values and store them.		
					meanCat = meanSum / processedDataCategory[i][j].size();
					minCat = minValue;
					maxCat = maxValue;
					medianCat = getMedian(processedDataCategory[i][j]);
					standardDeviationCat = getStandardDeviation(processedDataCategory[i][j], medianCat);

					// Reset values.
					meanSum = 0.0;
					minValue = 999.0;
					maxValue = -999.0;
				}
				
				// Save baked values.
				result.setMaxCategory(maxCat);
				result.setMinCategory(minCat);
				result.setMeanCategory(meanCat);
				result.setMedianCategory(medianCat);
				result.setStandardDeviationCategory(standardDeviationCat);
				
				meanCat = Double.NaN;
				minCat = 999.0;
				maxCat = -999.0;;
				medianCat = Double.NaN;
				standardDeviationCat = Double.NaN;

				categoryStatistics[i][j] = result;
			}
		}

		// Add categorical statistics to the global statistics object.
		stats.setCategoryStatistics(categoryStatistics);
		
		return stats;
	}

	/**
	 * Method to compute the median value.
	 * @param values
	 * @return double with the median value.
	 */
	private double getMedian(ArrayList<Double> values){
		// Do not compute if we have less than 1 value in the data.
		if(values.size() > 0){
			// Sort value.
			Collections.sort(values);

			// Compute median.
			if(values.size() % 2 == 1){
				return values.get((values.size() + 1)/ 2-1);
			}
			else{
				double lower = values.get((values.size()/ 2)-1);
				double upper = values.get(values.size()/ 2);

				return (lower + upper) / 2.0;
			}
		}	
		else{
			return Double.NaN;
		}
	}

	/**
	 * Method to compute the standard deviation.
	 * @param values
	 * @param mean
	 * @return double with the standard deviation value.
	 */
	private double getStandardDeviation(ArrayList<Double> values, double mean){
		// Do not compute if we have less than 1 value in the data.
		if(values.size() > 0){
			// Calculate the sum of squares.
			double sum = 0;
			for ( int i = 0; i < values.size() ; ++i){
				final double v = values.get(i) - mean;
				sum += v * v;
			}

			// Change to ( n - 1 ) to n because we have complete data.
			return Math.sqrt( sum / ( values.size() - 1 ) );
		}
		else{
			return Double.NaN;
		}
	}

	/**
	 * Method to return a sub array with only required medical data.
	 * @param daysCount
	 * @return a trimmed(dates) array of medical data.
	 * @throws ParseException
	 */
	private MedicalDataItem[] trim(int daysCount) throws ParseException{
		// Variables.
		List<MedicalDataItem> trimmedData = new ArrayList<MedicalDataItem>();
		DateTimeFormat dateFormat = DateTimeFormat.getFormat("dd.MM.yyyy");

		// Get the two dates in between which we want to select data.
		// Getting current date.
		Date dateNow = new Date();
		Date targetDate = dateNow;

		// Getting the targeted date in 'daysCount' days.
		CalendarUtil.addDaysToDate(targetDate, (daysCount + 1) * (-1));	// Invert 'daysCount' to create a 

		// Go through all the medical data and check if date is between our two
		// reference dates.
		for(MedicalDataItem data : globalData){
			if(data != null){
				Date dataDate = dateFormat.parse(data.getDate());	

				if(dataDate.compareTo(targetDate) >= 0){
					trimmedData.add(data);
				}
			}
		}

		// Convert list to array of medicalDataItems.
		MedicalDataItem[] trimmedDataArray = new MedicalDataItem[trimmedData.size()];

		for(int i = 0 ; i < trimmedDataArray.length; ++i){
			trimmedDataArray[i] = trimmedData.get(i);
		}

		return trimmedDataArray;
	} 

	/**
	 * Method to return a sub array with only required medical data in a selected
	 * time range.
	 * @param fromDate
	 * @param toDate
	 * @return a trimmed(dates) array of medical data.
	 * @throws ParseException
	 */
	private MedicalDataItem[] trimCustom(Date fromDate, Date toDate) throws ParseException{
		// Variables.
		List<MedicalDataItem> trimmedData = new ArrayList<MedicalDataItem>();
		DateTimeFormat dateFormat = DateTimeFormat.getFormat("dd.MM.yyyy");

		Date dateNow = fromDate;
		Date targetDate = toDate;

		// Go through all the medical data and check if date is between our two
		// reference dates.
		for(MedicalDataItem data : globalData){
			if(data != null){
				Date dataDate = dateFormat.parse(data.getDate());	

				if(dataDate.compareTo(targetDate) >= 0 && dataDate.compareTo(dateNow)<= 0){
					trimmedData.add(data);
				}
			}
		}

		// Convert list to array of medicalDataItems.
		MedicalDataItem[] trimmedDataArray = new MedicalDataItem[trimmedData.size()];

		for(int i = 0 ; i < trimmedDataArray.length; ++i){
			trimmedDataArray[i] = trimmedData.get(i);
		}

		return trimmedDataArray;
	}
}